程式語言在某種分類上可以分為低階語言與高階語言。低階語言(如C語言)提供了從作業系統規畫一塊記憶體來用的功能,不過程式也要自己負責在記憶體不用時還回去給作業系統,才不會造成記憶體漏水(memory leak)。
不同於低階語言,像JavaScript這種高階語言,記憶體的管理是由系統負責的,在新增物件時會從系統得到一塊記憶體,但在物件被丟棄之後,程式設計師只能相信系統會幫忙處理,但無法得知一段記憶體到底什麼時候會被系統回收。
系統回收記憶體的策略,是去檢查這個變數是不是能從JavaScript的核心尋著變數分支找得到。比如我們寫了一個app.ts,這個檔案會被當成一個模組放在核心的某個地方,所以這個模組永遠不會被系統回收。那麼在這個模組裏寫了一個變數並放入一個物件,那麼這個物件就可以從核心通過模組裏的變數找到,因此不會被系統回收。接著我們把這個變數設定為null,那原本放在該變數的物件就無法再從核心藉由任何途徑找到,於是這個物件就會被系統放入資源回收桶,等待下次回收車來的時候,將這段記憶體載回去給系統再利用。
這個記憶體回收系統感覺很棒呀!是很棒沒錯,記憶體能被回收再利用,那麼我們的遊戲就能一直擁有足夠的資源,保持運作的順暢。
不過問題就在於,我們不知道回收車哪時候才會來。
不管是以前的Flash還是現在的HTML5,網頁上的記憶體回收車,都是在CPU使用率低的時候才會開出來。也就是說,如果遊戲一直處在繁忙的狀態,那麼回收車就不會來,記憶體會越用越兇,即使程式設計師都做好記憶體回收的標記工作,但回收車不來,就只能乾等,最後資源越來越少,導致遊戲越跑越慢,直到最後系統受不了,直接來個網頁凍結(freeze)一秒,把回收車開出來,記憶體收一收,再讓網頁繼續。
沒有人會想要這樣的遊戲體驗吧,可是JavaScript就是不讓你自己管理記憶體呀!這怎麼辦?
系統不給?那就自己做!
要自製資源回收池,需要建立可回收物件的介面。
// 定義可回收物件的介面
interface IRecyclable {
// 被回收時要呼叫的重置函式
reset(): void;
// 查看這物件是不是已毀滅,無法再利用
destroyed: boolean;
}
接著就可以來設計資源回收池的類別。
/** 建立一個回收池
* <T>是TypeScript宣告泛型的方法,
* 可以在設計這個類別時,先假定有一個T的型別,
* 在實際創建實體時,再動態指定T的真正型別,
* 本文最後會再詳加說明。
* 這裏的<T extends IRecyclable>表示這個回收池
* 是專案設計給T這個型別,
* 而T必須符合IRecyclable規定的介面。
*/
class RecyclePool<T extends IRecyclable> {
// 被回收的物件列表
items: T[] = [];
// 從池裏拿一個出來用
getInstance(): T {
// 如果池裏還有剩的
if(this.items.length) {
// 從池子裏拿一個出來
let instance = this.items.pop();
// 如果這物件沒壞
if (!instance.destroyed) {
// 那重置一下物件,就可以給人用了
instance.reset();
return instance;
}
}
// 如果池裏沒有堪用的物件就回傳null
return null;
}
// 回收物件的函式
recycle(item: T): void {
// 若物件沒壞,而且還不在items陣列裏
if (!item.destroyed && !this.items.includes(item)) {
// 丟進回收池
this.items.push(item);
}
}
}
有了上面對回收物及回收池的定義之後,接著來設計一個可回收的物件類別來測試。
我們來製造橘色彈的,這些橘色彈會在畫面上播放炸開的動畫。我們待會兒會每幾個毫秒就放一顆給他爆。
// 建立一個變數,用來記錄我們創建了幾枚炸彈
let bombCreated = 0;
// 可回收的炸彈類別,必須實作可回收物件的介面
class Bomb implements IRecyclable {
// 建一個static,專門給Bomb用的回收池
// 文章最後會對static再加說明
private static pool = new RecyclePool<Bomb>();
// 寫一個static函式,用來取出一個炸彈
static getInstance(): Bomb {
// 先去池裏看看有沒有
let instance = Bomb.pool.getInstance();
// 如果沒有就建一個新的
if (!instance) {
instance = new Bomb();
instance.reset();
}
return instance;
}
// 為炸彈畫一個橘色圓,這裏使用的是CG的繪圖功能
circle = draw.circle(0, 0, 15, { fillColor: 0xFF6600, lineThickness: 0 });
// 被回收時要呼叫的重置函式
reset(): void {
// 將circle重新設定為一開始的狀態
this.circle.scale.set(1);
this.circle.alpha = 1;
}
// 一個唯讀的屬性,查看這物件是不是已毀滅,無法再利用
get destroyed(): boolean {
// 如果圖形因某些不明原因壞了,就不要再回收這個物件了
return this.circle.destroyed;
}
// 建構子
constructor() {
// 在新增Bomb時,將bombCreated加一
bombCreated++;
// 在控制台列印出來
console.log(`第${bombCreated}枚新炸彈被造出來了!`);
}
// 開始炸彈的動畫
start(): void {
// 在畫面上隨機選一點放炸彈
this.circle.x = rng.nextBetween(40, 600);
this.circle.y = rng.nextBetween(40, 440);
// 用tween物件產生動畫,並在動畫結束時回收
new TWEEN.Tween(this.circle)
.to( // 設定動畫的目標
{
alpha: 0, // 不透明度要變化到0
scaleX: 2, // x放大到2
scaleY: 2, // y放大到2
},
1000
)
.onComplete(() => { // 設定動畫完成後要做的事
// 把我拿去回收
Bomb.pool.recycle(this);
})
.start(); // 開始播放
}
}
// 設定每120毫秒放一顆炸彈
setInterval(function () {
Bomb.getInstance().start();
}, 120);
由以上的示範程式中發現,無論畫面上炸了幾千顆橘色彈,整個系統總共只建構了九枚全新彈,不賴吧!
在設計函式的時候,有時我們想設定回傳物件的型別要和傳入的參數一樣,但是我們又希望參數能接受很多種型別,那可能可以這樣寫。
function add(value1: number|string, value2: number|string): any {
return value1 + value2;
}
不過這樣寫的話,參數的型別在函式回傳的時候中就遺失了,我們會無法確定回傳出來的東西到底是什麼。
為了更清楚地定義函式,我們可以使用泛型來動態定義參數與回傳值的型別。
function add<T>(value1: T, value2: T): T {
return value1 + value2;
}
我們宣告add<T>
的時候,就是告訴函式的使用者『你在用這個函式時,可以決定T是什麼,而函式回傳的值也一樣限制要是T這個型別。』
// 我們限制T一定要是一個number
// 因此除了傳進去的參數一定要是數字
// 也同時保證了傳出來的值也是數字
let result1 = add<number>(1, 2);
console.log("type of result1 = " + (typeof result1));
// 上面那一行會列印出 > type of result1 = number
// 同一個函式,但這次我們限制T一定要是一個string
let result2 = add<string>("haska ", "rocks");
console.log("type of result2 = " + (typeof result));
// 上面那一行會列印出 > type of result2 = string
用類似方法,我們也可以在類別或介面上上加上泛型,並且還能用extends來縮小T型別的適用範圍。
/** 定義RecyclePool的類別,而且可以用T來實作相關的函式或屬性
* 其中T一定要是一個符合IRecyclable或IRecyclable延伸出來的類別
*/
class RecyclePool<T extends IRecyclable> {
items: T[] = [];
getItem(index: number): T {
return this.items[index];
}
}
泛型主要應用在彈性需求較高的函式庫設計上,更進一步的說明可以參考TypeScript官網對Generics的介紹。
在類別(class)裏面寫靜態變數(static),可以在不建立類別實體時,直接調用這些靜態變數或函式。
比如說我們有下面這個類別。
class Robot {
// 靜態變數,可用 Robot.firstRule 調用
static firstRule = "機器人不得傷害人類";
// 靜態函式,可用 Robot.pickName() 調用
static pickName(): string {
// 隨機選一個名字
return "Robert_" + Math.round((Math.random() * 99999));
}
// 使用靜態函式幫這個實體取一個名字
name = Robot.pickName();
// 把參數給的句子說出來
say(something: string): void {
console.log(something);
}
}
其中的name是一個Robot的屬性,say()則是Robot可以用的函式,要使用這兩個屬性及函式,需要先有一個Robot的實體才行。
let johnnyFive = new Robot();
johnnyFive.say("我的名字是" + johnnyFive.name);
但是靜態屬性或函式就不一樣了。靜態屬性及函式是屬於Robot這個類別的,所以想要調用其中的靜態屬性,就要用 Robot.firstRule 這種寫法。
johnnyFive.say(Robot.firstRule);